package gamelib;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * Loads resources from the filesystem and store them in a map.
 * @author clementj
 *
 */
public class BnbResourceManager {
	private String rootPath;
	/**
	 * Associate a key to a file (full path).<br>
	 * Example : "background-title" => "/path/to/resources/background-title.png"
	 */
	private Map<String, String> fileMap;
	/**
	 * Associate a key to a resource data (image, music, etc.).
	 */
	private Map<String, Object> resourceMap;
	/**
	 * Associate a file extension to a loader.
	 */
	private Map<String, ResourceLoader> loaderMap;

	/**
	 * Used to inform that a resource could not be found, neither in the map nor on the filesystem.
	 */
	@SuppressWarnings("serial")
	public static final class ResourceNotFoundException extends Exception {
		public ResourceNotFoundException(final String message) {
			super(message);
		}
	}
	/**
	 * Used to inform that a resource file extension is not supported (no loader available).
	 */
	@SuppressWarnings("serial")
	public static final class UnsupportedResourceExtension extends Exception {
		public UnsupportedResourceExtension(final String message) {
			super(message);
		}
	}

	public BnbResourceManager () {
		init();
	}

	public BnbResourceManager (final String rootPath) {
		init();
		setRootPath(rootPath);
	}

	/**
	 * Initializes the class members.
	 */
	private void init() {
		fileMap = new HashMap<String, String>();
		resourceMap = new HashMap<String, Object>();
		loaderMap = new HashMap<String, ResourceLoader>();
	}


	public String getRootPath() {
		return rootPath;
	}

	/**
	 * Changes the root path of the resources to manage.<br>
	 * This clears and rebuilds the file map, and clear the resource map.
	 * @param rootPath
	 */
	public void setRootPath(final String rootPath) {
		this.rootPath = rootPath;
		fileMap.clear();
		resourceMap.clear();
		rescanFS();
	}

	/**
	 * Register a new loader for the specified extension. <br>
	 * If a loader already exists, it is replaced and a warning is raised in the console.
	 * @param extension
	 * @param loader
	 */
	public void registerLoader (final String extension, ResourceLoader loader) {
		if (null != loaderMap.get(extension)) {
			System.out.println("Warning : previous loader for extension \""+extension+"\" has been replaced.");
		}

		loaderMap.put(extension, loader);
	}

	/**
	 * Rescans recursively the whole resource path (from rootPath), and build the file map.<br>
	 * Ill-formed filenames are ignored and a warning is raised in the console.<br>
	 * Typical ill-formed files are files which are not of the form "prefix.extension", as it can be found on unix systems.<br>
	 * Example : "myimage.png" is correct, while "myimage" is not.
	 * 
	 * Only files having an extension associated to a loader are put into the map. Therefore, 'rescanFS' must be called *after* all the loaders registration (through registerLoader()).
	 * 
	 * @see registerLoader()
	 */
	private void rescanFS() {
		Collection<File> fileList = new ArrayList<File>();
		addFilesRecursively(new File("."), fileList);
	}

	/**
	 * Recursively visit the given file, and loads canonical path into the file map if a loader is found for the file extension.
	 * @param file
	 * @param fileList
	 */
	private void addFilesRecursively(File file, Collection<File> fileList) {
		final File[] children = file.listFiles();
		if (children != null) {
			for (File child : children) {
				try {
					String canonicalPath = child.getCanonicalPath();
					String key = getKey(canonicalPath);
					String extension = getExtension(canonicalPath);
					
					// If the extension could be extracted.
					if (null != key && null != extension) {
						// If a loader is found, add the file to the file map.
						if (null != loaderMap.get(extension)) {
								fileMap.put(key, canonicalPath);
						}
					}
				}
				catch (IOException e) {
					System.err.print(e.getMessage());
					return;
				}

				fileList.add(child);
				addFilesRecursively(child, fileList);
			}
		}
	}

	/**
	 * Get a resource given its key.<br>
	 * If not in the map, loads it from the disk.
	 * @param key
	 * @return the desired data.
	 * @throws ResourceNotFoundException
	 * @throws UnsupportedResourceExtension
	 * @throws ResourceLoadException 
	 */
	public Object getObject (final String key) throws ResourceNotFoundException, UnsupportedResourceExtension, ResourceLoadException {
		Object data = resourceMap.get(key);
		if (null == data) {
			data = load(key);
		}
		return data;
	}

	/**
	 * Load a resource in the map, given its key.<br>
	 * A file with name [key].[ext] is searched in the file map.<br>
	 * An exception is thrown whenever an error occur during the load.<br>
	 * Example : if key is "background-title" then search for a file "/path/to/resources/background-title.[extension]".
	 * 
	 * @param key
	 * @return the desired data.
	 * @throws ResourceNotFoundException
	 * @throws UnsupportedResourceExtension
	 * @throws ResourceLoadException
	 * @see rescanFS
	 */
	private Object load(final String key) throws ResourceNotFoundException, UnsupportedResourceExtension, ResourceLoadException {
		String filePath = fileMap.get(key);
		if (null == filePath) {
			throw new ResourceNotFoundException("Resource : \""+key+"\" not found. Try calling \"rescanFS()\".");
		}

		String extension = getExtension(filePath);

		// Should never happen if the fileMap has been built properly with rescanFS().
		if (null == extension) {
			return null;
		}

		ResourceLoader loader = loaderMap.get(extension);
		if (null == loader) {
			throw new UnsupportedResourceExtension("Resource extension : \""+extension+"\" not supported. No loader found.");
		}

		// May trigger a ResourceLoadException.
		Object resource = loader.load(filePath);
		resourceMap.put(key, resource);

		return resource;
	}

	/**
	 * Given a file path, get the extension of the file.
	 * @param filePath
	 * @return the file extension or null if the filePath was malformed.
	 */
	private static String getExtension(final String filePath) {
		int indexOfPoint = filePath.lastIndexOf(".");

		if (indexOfPoint > 0) {
			return filePath.substring(indexOfPoint+1);
		}

		return null;
	}
	
	/**
	 * Given a file path, get the key of the file.
	 * @param filePath
	 * @return the file key or null if the filePath was malformed.
	 */
	private String getKey(String filePath) {
		
		int indexOfSep = filePath.lastIndexOf(System.getProperty("file.separator"));
		int indexOfPoint = filePath.lastIndexOf(".");

		if (indexOfSep > 0 && indexOfPoint > 0) {
			return filePath.substring(indexOfSep+1, indexOfPoint);
		}

		return null;
	}
}
